PROJET - Polymorphisme ********************** Nous vous proposons de compléter le développement d'un logiciel de dessin vectoriel. Cet outil permet de dessiner des formes géométriques simples et de les modifier par la suite : taille, couleur... Nous allons tout d'abord présenter l'architecture du projet actuel. Les classes de base =================== V2 -- Cette classe fournit une structure pour stocker des coordonnées 2D de points ou de vecteurs. .. csv-table:: :header: "Type", "Nom", "Description" :widths: 10, 10, 20 Constructeurs, "V2(x,y)", valeurs entières Opérateurs de base, "\+ - * == <<", Test, isInside(...), voir ci-dessous Utilitaire, getPLH(...), voir ci-dessous La fonction **isInside** détermine si le point courant est à l'intérieur du rectangle défini par le point bas/gauche *P* et un couple *Largeur/Hauteur* : .. image:: PLH.jpg :align: center :scale: 30% La fonction **getPLH** à partir de deux sommets opposés d'un rectangle, cette fonction retourne le sommet bas/gauche *P* et un couple *Largeur/Hauteur* .. image:: PLH2.jpg :align: center :scale: 50% Graphics -------- Lorsque vous aurez des affichages à effectuer, vous disposerez d'un objet Graphics offrant toutes les fonctions pour dessiner à l'écran. .. csv-table:: :header: "Type", "Nom", "Description" :widths: 10, 10, 20 Information, "GetWindowSize()", Retourne la taille de la fenêtre de l'application Initialisation, "clearWindow(...)", Efface le contenu de la fenêtre Texte, "drawStringFontRoman(...)", Affiche un texte Texte, "drawStringFontMono(...)", Affiche un texte Dessin, "setPixel(...)", Positionne la couleur d'un pixel Dessin, "drawLine(...)", Trace un segment Dessin, "drawPolygon(...)", Trace un polygone Dessin, "drawRectangle(...)", Trace un rectangle Dessin, "drawCircle(...)", Trace un cercle Sprite, "drawRectWithTexture(...)", Dessine une image dans le rectangle donnée .. note:: Pour le dessin d'une image, le fichier doit être enregistré au format png et stocké dans le répertoire de projet au coté des fichiers .cpp/.h. Il suffit ensuite de transmettre à la fonction *drawRectWithTexture* le nom du fichier avec l'extension png, sans le chemin complet. Color ----- Dans tout le programme, on utilise une structure *Color* avec des valeurs normalisées entre 0 et 1. .. csv-table:: :header: "Type", "Nom", "Description" :widths: 10, 10, 20 Constructeur, "Color(float r,float g, float b)", Valeurs RGB entre 0 et 1 Constructeur, "Color(float r,float g, float b, float a)", Paramètre alpha de transparence Construction, "ColorFrom255(int r, int g, int b)", Valeurs RGB entre 0 et 255 Construction, "ColorFromHex(int hexCode)", valeur HTML : 0x12B344 Constantes, Color::White, "Black, White, Red, Green, Blue, Magenta, Cyan, Yellow, Gray" Les classes du projet ===================== ObjAttr ------- La plupart des éléments graphiques : rectangle, cercle, polygone, texte possèdent ces 4 paramètres : .. csv-table:: :header: "Type", "Nom", "Description" :widths: 10, 10, 20 Attribut, borderColor , Couleur de trait Attribut, thickness, L'épaisseur du bord Attribut, interiorColor, Couleur de fond Attribut, isFilled, Intérieur opaque ? Il devient alors judicieux de les regrouper dans une structure *ObjAttr* pour améliorer la lisibilité du code. .. note:: Il existe une exception : le segment qui ne requiert que 2 paramètres : couleur et épaisseur. On pourrait faire le choix d'optimiser la mémoire et de faire un cas particulier pour cet élément. Mais, le projet étant assez conséquent, on choisit de simplifier sa structure. Pour cela, on harmonise tous les éléments graphiques en leur donnant un paramètre *ObjAttr*, segment y compris. EvenType -------- Notre application est du type **event-driven**. Ainsi, l'exécution du code est déclenchée par des actions de l'utilisateur telles que des clics de souris ou des pressions sur les touches. Si l'utilisateur ne fait rien, l'application n'exécute aucun code. Il existe 5 évènement principaux listés dans l'énumération **EvenType** : .. csv-table:: :header: "Type", "Nom", "Description" :widths: 10, 10, 20 Enum, MouseMove, déclenché lors des déplacements de la souris Enum, MouseDown, déclenché par l'appui sur le bouton 0-1-2 de la souris Enum, MouseUp, déclenché par le relâchement du bouton 0-1-2 de la souris Enum, KeyDown, déclenché par l'appui sur une touche Enum, KeyUp, déclenché lorsque la touche est relâchée Event ----- Pour modéliser les évènements, on aurait pu faire une hiérarchie avec une classe mère Event et 5 classes filles correspondant aux 5 type d'évènements. Cependant, ces classes ne contiennent aucune fonction et elles ne se différencient que par leurs attributs. On choisit une **modélisation par classe universelle** (universal class modeling). Pour cela, on crée une classe unique regroupant tous les attributs nécessaires pour couvrir plusieurs cas d'utilisation. Au lieu de définir une hiérarchie de classes, une seule classe contient tous les attributs possibles, même si certains ne seront pas utilisés dans certaines configurations. Attributs de la classe **Event** : .. csv-table:: :header: "Type", "Nom", "Description" :widths: 10, 10, 20 Attribut, "(x,y)", Position de la souris pour l'évènement MouseMove Attribut, info, MouveDown-MouseUp : id du bouton de la souris , info, KeyDown-KeyUp : la touche appuyée A titre indicatif, chaque évènement généré par le système produit un affichage dans la fenêtre console placée en arrière : .. image:: event.png :align: center :scale: 50% .. note:: On remarque que pour l'évènement MouseDown, les informations (x,y) sont incohérentes car non transmises. .. warning:: On peut utiliser une classe universelle car ce cas s'y prête bien. En effet, une quarantaine d'évènements sont générés par seconde au maximum et ils sont destinés à être mis à la poubelle une fois leur traitement terminé. Si un objet Event est certes un peu plus volumineux par accumulation d'attributs inutiles, on perd que quelques octets au final. Button ------ Dans notre fenêtre graphique, des boutons vont nous permettre de sélectionner un outil de dessin comme : l'outil rectangle, l'outil cercle ou l'outil sélection. Une première option consiste à mettre en place une hiérarchie issue de la classe *Button* avec une fonction polymorphe *OnClick*. Dans cette optique, la classe fille *RectangleButton* va redéfinir la méthode *OnClick* pour activer l'outil rectangle. Cependant cette approche a un problème. En effet, on doit éviter de créer des classes si peu de différences existent entre elles. Ainsi, on ne crée pas une classe *RectangleRouge* et une classe *RectangleVert*, on crée une seule classe *Rectangle* avec un paramètre *couleur*. Lorsque l'on analyse notre hiérarchie de *Button*, seule l'action effectuée par la méthode *OnClick* change, si l'on pouvait faire en sorte que cette action soit un attribut de la classe *Button*, nous n'aurions pas besoin d'une hiérarchie. Heureusement, la STL, dans sa librairie **, fournit une classe template *std::function* permettant d'affecter une fonction à une variable ! Voici un exemple : .. code-block:: #include #include using namespace std; int mini(int a,int b) { if (ab) return a; return b; } int main() { function myfnt; myfnt = mini; cout << myfnt(4,5) << endl; myfnt = maxi; cout << myfnt(4,5) << endl; } >> 4 >> 5 Ainsi, une seule classe *Button* peut être utilisée pour tous les outils : .. csv-table:: :header: "Type", "Exemple", "" :widths: 10, 10, 20 Info, getPos(), Donne la position du coin bas/gauche du bouton Info, getSize(), Donne la taille H/V du bouton Constructeur, Button(...), "Nom, dimension, fichier image, fonction à appeler lors du clic" Méthode , manageEvent(...), Fonction traitant un évènement utilisateur Méthode, Draw(...), Dessine le bouton à l'écran ObjGeom ------- L'application affiche différentes formes géométriques : cercles, rectangles, segments, polygones... Nous choisissons de construire une hiérarchie, ceci pour deux raisons : * Les différentes types de formes contiennent des attributs spécifiques * La fonction d'affichage est polymorphe car l'affichage dépend de la classe de l'objet : un objet rectangle s'affiche comme un rectangle, un objet cercle comme un cercle... .. csv-table:: :header: "Type", "Exemple", "" :widths: 10, 10, 20 Constructeur, ObjGeom(...), Paramètres définissant la forme Méthode, Draw(...), Affichage polymorphe Tool ---- L'application permet de dessiner différentes formes géométriques. Pour chacune d'elles, l'utilisateur va sélectionner un outil de tracé. Comme précédemment, nous choisissons de construire une hiérarchie d'outils : .. csv-table:: :header: "Type", "Nom", "Description" :widths: 10, 10, 20 Constructeur, Tool(), Pas de paramètre spécifique Méthode, ProcessEvent(...), Fonction polymorphe traitant les évènements clavier/souris pour la construction de la forme Méthode, Draw(...), Affichage polymorphe .. note:: Pourquoi avoir une méthode d'affichage associée aux outils ? Seuls les objets géométriques sont affichés à l'écran ! Non, pas exactement. Notre application est interactive, c'est à dire que lorsque nous allons tracer un cercle, nous allons cliquer puis faire glisser la souris pour que le cercle grossisse et se positionne à l'endroit désiré. Lors de la construction, l'outil affiche donc un tracé temporaire permettant de positionner la future forme correctement. Le tracé peut être fait en pointillés ou avec une couleur légèrement transparente. Une fois satisfait, l'utilisateur relâche le bouton de la souris et l'outil créé alors l'objet géométrique correspondant et le stocke dans la scène avec les autres. .. warning:: Il existe une association 1-1 entre la classe d'un objet géométrique et la classe de l'outil associé. On trouve ainsi une classe *ObjRectangle* qui correspond à un objet géométrique de la scène et un outil *ToolRectangle* qui correspond à l'outil de tracé des rectangles. Il s'agit de classes différentes avec des rôles différents. Cependant, l'association 1-1 pourra prêter à confusion. Le modèle : Model-View-Update ============================= Vous avez peut être entendu parler des architectures MVC pour ceux qui ont travaillé sur des interfaces web. Nous allons utiliser un modèle plus simple appelé MVU : * Model : contient toutes les données de l'application * View : à partir des données contenues dans le *Model*, affiche l'interface de l'application * Update : suite à un évènement clavier/souris met à jour le *Model* L\'*Update*, aussi appelé : la *Logique*, traite propage un évènement utilisateur (clavier/souris) jusqu'à l'entité devant le gérer. Par exemple, un clic souris sur un bouton change l'outil en cours. L'entité effectue son traitement et modifie les données du *Model*, par exemple, en ajoutant un rectangle dans la scène. A retenir : * L\'*Update* modifie les données du *Model* * L\'*Update* ne produit aucun affichage L'étape de *View* lit les données du modèle sans apporter aucune modification et affiche l'interface de l'application : * La *View* lit en lecture seule les données du *Model* * La *View* n'interagit pas avec le l\'*Update* * La *View* est seule en charge de l'affichage Pourquoi le modèle MVU convient-il mieux aux applications graphiques ? A ce niveau, il faut savoir qu'il y a deux points d'entrée dans notre programme présents dans le fichier **eleves.cpp** : * La fonction *ProcessEvent(...)* qui reçoit un évènement de l'utilisateur : clavier souris * La fonction *DrawApp(...)* appelée par le système Il faut bien comprendre que ces deux appels ne sont pas synchronisés. La fonction *ProcessEvent* n'est jamais appelée si l'utilisateur ne touche pas le clavier et la souris et ses appels se multiplient lorsque la souris bouge. D'un autre coté, la fonction *DrawApp()* dépend du système (Windows) qui se charge de l'appeler au moment opportun : déplacement de la fenêtre, agrandissement de la fenêtre, fin de l'appel d'une fonction *ProcessEvent*. Voici un schéma qui résument les interactions : .. image:: MVU.png :align: center :scale: 50% Model ----- La classe *Model* est instanciée une fois lors du lancement du programme. Cette instance est ensuite transmises aux deux fonctions principales : *ProcessEvent()* et *DrawApp()* : * void ProcessEvent(..., Model & Data) * void DrawApp(..., **const** Model & Data) On trouve dans la classe *Model* toutes les informations décrivant notre programme : .. csv-table:: :header: "Type", "Nom", "Description" :widths: 10, 10, 20 Attribut, currentTool, L'outil de tracé actif Attribut, currentMousePos, Dernière position connue de la souris Attribut, drawingOptions, "Les attributs de dessin : couleur de trait, de fond, épaisseur..." Attribut, LObjets, La liste des objets géométriques présents dans la scène Attribut, LButtons, La liste des boutons présents dans l'interface DrawApp ------- Cette fonction est le point d'entrée de la demande d'affichage de notre application. Sa lecture est très instructive et nous détaille comment s'affiche la fenêtre de travail. Vous n'aurez normalement pas de modifications à faire, nous la présentons juste à titre pédagogique : .. code-block:: void DrawApp(Graphics& G, const Model & D) { // fond noir G.clearWindow(Color::Black); // dessin de toutes les formes géométriques for (auto& Obj : D.LObjets) Obj->draw(G); // dessin des boutons for (auto& myButton : D.LButtons) myButton->draw(G); // dessin de l'outil courant si construction en cours D.currentTool->draw(G,D); // dessin du curseur souris drawCursor(G, D); } ProcessEvent ------------ Cette fonction est le point d'entrée d'un évènement utilisateur : clavier / souris. Son process est assez basique : * Elle parcourt les boutons de l'interface pour savoir si un clic a été effectué sur l'un d'eux. * => Si c'est le cas, elle transmet l'évènement au bouton pour traitement * Si aucun bouton n'a intercepté l’événement, l'évènement est transmis à l'outil courant pour traitement Le projet ========= Les outils ---------- Pour effectuer un tracé interactif, il faut pouvoir gérer une machine à états pour chaque outil. Par exemple pour l'outil *Segment*, nous avons : * Deux états symbolisés par des ronds : * *WAIT* : où l'outil n'est pas actif * *INTERACT* : où l'utilisateur déplace la souris pour tracer son segment * Des transitions symbolisées par des flèches : * Depuis l'état *WAIT*, l'appui sur le bouton gauche de la souris fait passer à l'état *INTERACT* * Depuis l'état *INTERACT*, le relachement du bouton gauche de la souris fait passer à l'état *WAIT* .. image:: etats.jpg :align: center :scale: 50% Ainsi, chaque outil dispose de son attribut interne : *currentState* qui indique l'état courant. Les différents états ont été définis dans l'énumération suivante : .. code-block:: enum State { WAIT, INTERACT }; Etape 1 : outil Rectangle ------------------------- Examiner la fonction *processEvent()* et *draw()* de la classe *ToolSegment*. Examinez leur logique interne. Dans la fonction *processEvent()* des *return* sont présents, pourquoi ? Vous avez suffisamment d'informations pour terminer l'outil *Rectangle*. Programmez ses fonctions *processEvent()* et *draw()*. Etape 2 : outil Cercle ---------------------- Voici les différentes tâches à remplir : * Trouvez l'image associée à cet outil dans le répertoire de projet * Créez la fonction qui change l'outil courant pour l'outil *Cercle* * Créez le bouton associé à cet outil dans la fonction *InitApp* présente dans *eleves.cpp*. * Associez ce bouton à la fonction de changement d'outil * Créez la classe dérivée de la classe mère *Tool* * Implémentez les fonctions *processEvent()* et *draw()* * Ajoutez un objet *Cercle* dans la liste des objets de la scène en tenant compte des paramètres de tracé courant .. warning :: Le clic de la souris positionne le centre du cercle. Ensuite, en déplaçant la souris, le curseur indique un point du cercle. Nous avons donc deux points qui définissent le cercle actuel : le centre et un point du bord. Etape 3 : outil RAZ ------------------- * Créez un bouton qui lorsque l'on appui dessus vide le contenu de la scène Etape 4 : outil Sélection/Suppression ------------------------------------- Voici les fonctionnalités à mettre en œuvre : * Créez un nouvel outil permettant de sélectionner un objet * Pour chaque objet, créez une fonction retournant une hitbox correspondant à un rectangle englobant cet objet * Lorsque l'utilisateur clique dans la scène, sélectionnez au plus **un** objet dont la hitbox contient le curseur * Si l'utilisateur reclique, la sélection courante est oubliée et une nouvelle sélection est activée * L'objet sélectionné doit être affiché différemment : couleur inversée, trait plus épais, encadré en rose... * Si l'utilisateur clique sur le bouton de l'outil suppression l'objet sélectionné est retiré de la scène Etape 5: outil Devant/Derrière ------------------------------ Pour cela, il faut utiliser des objets superposés avec des fonds pleins. Voici les fonctionnalités à mettre en œuvre : * Si l'utilisateur clique sur le bouton *Devant*, l'objet sélectionné monte d'un cran dans la liste des objets * Si l'on clique plusieurs fois, l'objet finit par recouvrir tous les autres * Symétriquement, on construit le bouton *Derrière*, qui fait descendre l'objet sélectionné dans la liste * Si l'on clique plusieurs fois, l'objet finit par être recouvert par tous les autres Etape 6 : options de tracé -------------------------- Ajoutez 4 boutons permettant de gérer : * La couleur de trait * La couleur de fond * L'épaisseur * La présence d'un fond ou non Pour chaque bouton, le clic change le paramètre courant pour le suivant. Par exemple, on peut partir du principe qu'il y a un choix parmi 7 couleurs, 4 épaisseurs. Le bouton affiche le choix courant : * Pour la couleur de trait : il dessine un trait de cette couleur à l'intérieur du bouton * Pour l'épaisseur : il dessine un trait de l'épaisseur correspondante à l'intérieur du bouton Etape 7 : outil Ligne Polygonale -------------------------------- * Ajoutez un nouvel outil pour tracer une ligne brisée * L'interaction est différente, chaque clic de la souris va ajouter un nouveau sommet à la ligne courante * Il faudra utiliser la touche Entrée ou le bouton de droite de la souris pour stopper la construction de la ligne Etape 8 : Save/Load ------------------- La sérialisation est le processus de transformation d'un objet en un format qui peut être facilement stocké. * Créez une fonction polymorphe *Serialize* qui traduit tout objet graphique en chaîne de caractères. Cette chaîne de caractères doit comporter suffisamment d'information pour permettre de reconstruire l'objet à posteriori. * Créez un bouton *Save* qui sauvegarde le contenu de la scène dans un fichier. Le nom du fichier peut être fixe. * Créez un bouton *Load* reconstruit la scène sauvegardée dans un fichier. Le nom du fichier peut être fixe. Voici un exemple de la classe stringstream qui peut s'avérer utile : .. code-block:: #include using namespace std; stringstream ss; // put data into the stream ss << 4.5 << ", " << 4 << " hello"; // convert the stream buffer into a string string str = ss.str(); Etape 9 : Undo -------------- * Ajoutez un bouton Undo permettant d'annuler la dernière action Plusieurs approches sont possibles. Nous en décrivons une utilisant la sérialisation : * A chaque modification de la liste des objets, effectuez une sérialisation de la scène dans un string * Archivez ces strings dans un container servant d'historique * Lors de l'appui sur le bouton *Undo* : videz la scène et remplacez là par la dernière sérialisation disponible Etape 10 : Edition des points ----------------------------- L'appui sur le bouton *Edition des Points* fait passer en mode édition l'ensemble des points présents dans les objets : * Chaque point est affiché dans la scène par dessus l'affichage courant * Lorsque l'on clique à proximité d'un point, on a alors la capacité de le déplacer en bougeant la souris Un nouvel appui sur le bouton *Edition des Points* réactive l'outil précédemment utilisé.